﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Web;
using System.Xml;

namespace Twitter
{
	/// <summary>
	/// Exposes operations related to the users' direct messages.
	/// </summary>
	public class Messages : IMessages
	{
		static ITraceSource tracer = Tracer.GetSourceFor<Messages>();
		AuthenticatedEndpoint twitterEndpoint;
		Uri baseUri;
		internal const int TopPage = 64;

		internal Messages(AuthenticatedEndpoint endpoint,
			Uri baseUri)
		{
			this.twitterEndpoint = endpoint;
			this.baseUri = baseUri;
		}

		/// <summary>
		/// Deletes the message with the given <paramref name="messageId"/>.
		/// </summary>
		/// <returns><see langword="true"/> if the message could be deleted; <see langword="false"/> otherwise.</returns>
		public bool Delete(string messageId)
		{
			Guard.ArgumentNotNullOrEmptyString(messageId, "messageId");

			// http://twitter.com/direct_messages/destroy/id.format
			string getUrl = String.Format("direct_messages/destroy/{0}.xml", messageId);

			tracer.TraceInformation("Deleting message {0} by invoking {1}",
				messageId, new Uri(baseUri, getUrl));

			try
			{
				twitterEndpoint.Execute(new Uri(baseUri, getUrl), HttpMethod.Delete);

				tracer.TraceEvent(TraceEventType.Verbose, 0,
					"Deleted message {0}", messageId);

				return true;
			}
			catch (Exception ex)
			{
				// Don't rethrow. It will be ignored.
				tracer.TraceError(ex, "Failed to delete message {0}", messageId);

				return false;
			}
		}

		/// <summary>
		/// Retrieves the direct messages sent to the authenticated user, with 
		/// older messages appearing first unlike twitter API.
		/// </summary>
		public IEnumerable<Message> GetAll()
		{
			// http://twitter.com/direct_messages.format
			string urlFormat = "direct_messages.xml?page={0}";

			int page = TopPage;
			int jump = page / 2;
			// It's actually 67, but leave space for UTF8 to UTF16 conversion if 
			// it ever happens, doubling the bytes.
			int nullLength = 150;

			tracer.TraceInformation("Determining top page to retrieve");

			while (jump != 0)
			{
				string getUrl = String.Format(urlFormat, page);
				Uri pageUri = new Uri(baseUri, String.Format(urlFormat, page));

				tracer.TraceEvent(TraceEventType.Verbose, 0, "Retrieving {0} to determine if it has content", pageUri);

				WebRequest request = twitterEndpoint.CreateRequest(pageUri);
				try
				{
					using (WebResponse response = request.GetResponse())
					{
						if (response.ContentLength < nullLength)
						{
							tracer.TraceEvent(TraceEventType.Verbose, 0, "Empty page {0}", pageUri);
							page = page - jump;
						}
						else
						{
							tracer.TraceEvent(TraceEventType.Verbose, 0, "Non-empty page {0}", pageUri);
							page = page + jump;
						}

						jump = jump / 2;
					}

				}
				catch (Exception ex)
				{
					Log(ex, "Failed to retrieve {0}", pageUri);
					throw;
				}
			}

			// Go from the last page in reverse order to the first
			// so that the bot can process older messages first.
			for (int i = page; i > 0; i--)
			{
				WebResponse response = null;
				Uri pageUri = new Uri(baseUri, String.Format(urlFormat, i));

				tracer.TraceInformation("Retrieving messages from {0}", pageUri);

				try
				{
					WebRequest request = twitterEndpoint.CreateRequest(pageUri);
					response = request.GetResponse();
				}
				catch (Exception ex)
				{
					Log(ex, "Failed to retrieve {0}", pageUri);
					throw;
				}

				using (response)
				{
					if (response.ContentLength > nullLength)
					{

						using (Stream responseStream = response.GetResponseStream())
						using (XmlReader reader = XmlReader.Create(responseStream))
						{
							var messages = PayloadReader.ReadMessages(reader).ToList();
							messages.Reverse();

							foreach (var message in messages)
							{
								yield return message;
							}
						}
					}
				}
			}
		}

		/// <summary>
		/// Retrieves the latest direct message sent to the authenticated 
		/// user.
		/// </summary>
		/// <returns><see langword="null"/> if no messages were found; the latest message otherwise.</returns>
		public Message GetLatest()
		{
			// http://twitter.com/direct_messages.format
			string getUrl = "direct_messages.xml";

			WebResponse response = null;
			Uri pageUri = new Uri(baseUri, getUrl);
			tracer.TraceInformation("Retrieving latest message from {0}", pageUri);

			try
			{
				WebRequest request = twitterEndpoint.CreateRequest(pageUri);
				response = request.GetResponse();

				using (response)
				{
					using (Stream responseStream = response.GetResponseStream())
					using (XmlReader reader = XmlReader.Create(responseStream))
					{
						var messages = PayloadReader.ReadMessages(reader);

						return messages.FirstOrDefault();
					}
				}
			}
			catch (Exception ex)
			{
				Log(ex, "Failed to retrieve {0}", pageUri);
				throw;
			}
		}

		/// <summary>
		/// Sends a direct message to the user with the given 
		/// <paramref name="screenName"/>.
		/// </summary>
		public Message Send(string screenName, string message)
		{
			Guard.ArgumentNotNullOrEmptyString(screenName, "userId");
			Guard.ArgumentNotNullOrEmptyString(message, "message");

			tracer.TraceInformation("Sending message: recipient={0}, text={1}", screenName, message);

			//http://twitter.com/direct_messages/new.xml;

			WebRequest request = twitterEndpoint.CreateRequest(new Uri(baseUri, "direct_messages/new.xml"));
			request.ContentType = "application/x-www-form-urlencoded";
			request.Method = "POST";

			byte[] data = Encoding.UTF8.GetBytes(
				"user=" + HttpUtility.UrlEncode(screenName) +
				"&text=" + HttpUtility.UrlEncode(message));
			request.ContentLength = data.Length;

			// Submit the data.
			using (Stream requestStream = request.GetRequestStream())
			{
				requestStream.Write(data, 0, data.Length);
				try
				{
					using (WebResponse response = request.GetResponse())
					using (Stream responseStream = response.GetResponseStream())
					using (XmlReader reader = XmlReader.Create(responseStream))
					{
						foreach (var messageResponse in PayloadReader.ReadMessages(reader))
						{
							tracer.TraceInformation("Retrieved sent message: Id={0}, Text={1}, Sender={2}, Recipient={3}",
								messageResponse.Id, messageResponse.Text, messageResponse.Sender.ScreenName, messageResponse.Recipient.ScreenName);

							return messageResponse;
						}
					}

				}
				catch (Exception ex)
				{
					Log(ex, "Failed to send message '{0}' to user {1}",
						message, screenName);
					throw;
				}
			}

			return null;
		}

		private void Log(Exception ex, string format, params object[] args)
		{
			WebException wex = ex as WebException;
			if (wex != null && wex.Response != null)
			{
				// TODO: improve error parsing.
				string responseError = new StreamReader(wex.Response.GetResponseStream()).ReadToEnd();
				tracer.TraceError(wex,
					"{0}. Response was:\n{1}",
					String.Format(format, args),
					responseError);
			}
			else
			{
				tracer.TraceError(ex, format, args);
			}
		}
	}
}
